using System.Collections.Generic;
using System.IO;
using System.Linq;
using System;
using System.Text.RegularExpressions;
using System.Xml;

using MyUtils;

namespace SolutionOrchestrator.Validators
{
	public class BadReferences : XmlFileValidator
	{
		protected override IEnumerable<Violation> Validate(XmlDoc doc, ValidationContext ctx)
		{
			var projectType = doc.SelectSingle("x:Project/x:PropertyGroup/x:ProjectTypeGuids");
			var isTestProject = projectType != null && "{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}".Equals(projectType.InnerText, StringComparison.InvariantCultureIgnoreCase);

			isTestProject = isTestProject || Path.GetFileNameWithoutExtension(doc.FileName).ToUpperInvariant().Contains("TEST");

			foreach (var item in GetReferences(doc, ctx))
			{
				FixableBadReference.AddCommonReference(item);

				if (item.IsGac)
				{
					if (!IsPossibleGAC(item, isTestProject))
					{
						yield return new FixableBadReference(doc.FileName, ctx, item)
						{
							Description = "Bad Reference To GAC: " + item.Include,
						};
					}
				}
				else
				{
					if (!PathHelper.FileExists(item.AbsoluteHint))
					{
						yield return new FixableBadReference(doc.FileName, ctx, item)
						{
							Description = string.Format("Referenced File Does Not Exist: {0} ({1})", item.HintPath, item.Include),
						};
					}
					if (!item.AbsoluteHint.StartsWith(ctx.SolutionDir, StringComparison.InvariantCultureIgnoreCase))
					{
						yield return new FixableBadReference(doc.FileName, ctx, item)
						{
							Description = string.Format("Reference To Outside: {0} ({1})", item.HintPath, item.Include),
						};
					}
				}
			}
		}

		internal class FixableBadReference : FixableViolation
		{
			static readonly Dictionary<string, object> _commonReferences = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
			static readonly Dictionary<string, Dictionary<string, string>> _commonReferencedFiles = new Dictionary<string, Dictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase);
			static readonly Dictionary<string, string> _commonDlls = new Dictionary<string, string>();

			static readonly List<FixableBadReference> _values = new List<FixableBadReference>();

			public static void ClearCaches()
			{
				lock (_commonReferences)
				{
					_commonReferences.Clear();
				}
				lock (_commonReferencedFiles)
				{
					_commonReferencedFiles.Clear();
				}
				lock (_commonDlls)
				{
					_commonDlls.Clear();
				}
			}

			public static void AddCommonReference(Reference reference)
			{
				if (reference == null)
				{
					throw new ArgumentNullException("reference");
				}

				if (reference.IsGoodReference)
				{
					lock (_commonReferences)
					{
						_commonReferences[reference.AbsoluteHint] = null;
					}

					var fileName = Path.GetFileNameWithoutExtension(reference.HintPath) + "";

					AddMap(fileName, reference.AbsoluteHint);
				}
			}


			public static void AddMap(string fileName, string absolutePath)
			{
				Dictionary<string, string> references;
				if (!_commonReferencedFiles.TryGetValue(fileName, out references))
				{
					_commonReferencedFiles[fileName] = references = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
				}
				references[absolutePath] = null;

				lock (_commonDlls)
				{
					_commonDlls[PathHelper.GetDirectoryName(absolutePath) + ""] = null;
				}

				lock (_values)
				{
					foreach (var val in _values)
					{
						val.Revalidate();
					}
				}
			}

			~FixableBadReference()
			{
				lock (_values)
				{
					_values.Remove(this);
				}
			}

			readonly string _fileName;
			readonly ValidationContext _ctx;
			readonly Reference _reference;

			public FixableBadReference(string fileName, ValidationContext ctx, Reference reference)
			{
				lock (_values)
				{
					_values.Add(this);
				}
				_fileName = fileName;
				FullPath = _fileName;
				_ctx = ctx;
				_reference = reference;
			}

			class ReferenceFix : Fix
			{
				readonly string _hintPath;

				public string HintPath
				{
					get { return _hintPath; }
				}

				readonly string _absolutePath;

				public string AbsolutePath
				{
					get { return _absolutePath; }
				}

				public ReferenceFix(string path, string absolutePath, bool recommended)
					: base(recommended)
				{
					_hintPath = path;
					_absolutePath = absolutePath;
				}

				public override string ToString()
				{
					return _hintPath;
				}

				public override bool Equals(object q)
				{
					return Equals(q as ReferenceFix);
				}

				public bool Equals(ReferenceFix fix)
				{
					if (fix == null)
					{
						return false;
					}
					return fix._hintPath == _hintPath;
				}

				public override int GetHashCode()
				{
					return _hintPath.GetHashCode();
				}
			}

			protected override IEnumerable<Fix> FixesAvailableCore
			{
				get
				{
					bool anySuggestions = false;
					// TODO check both file name and 'inclide'
					var fileName = Path.GetFileNameWithoutExtension(_reference.HintPath);
					if (string.IsNullOrWhiteSpace(fileName))
					{
						// bad gac
						fileName = Regex.Match(_reference.Include, @"(?'dll'[^,]+)").Groups["dll"].Value;
					}

					Dictionary<string, string> dic;
					if (_commonReferencedFiles.TryGetValue(fileName, out dic))
					{
						foreach (var absolutePath in dic.Keys)
						{
							var goodRef = BuildPath(absolutePath);
							if (goodRef != null)
							{
								anySuggestions = true;
								yield return new ReferenceFix(goodRef, absolutePath, true);
							}
						}
					}

					if (!anySuggestions)
					{
						//var absSuggested = new List<string>();
						string[] commonReferenceKeys;
						lock (_commonReferences)
						{
							commonReferenceKeys = _commonReferences.Keys.ToArray();
						}
						foreach (var absolutePath in commonReferenceKeys)
						{
							var goodRef = BuildPath(absolutePath);
							if (goodRef != null)
							{
								//absSuggested.Add(absolutePath);
								yield return new ReferenceFix(goodRef, absolutePath, false);
							}
						}

						// splitter
						yield return null;

						string[] commonDllsKeys;
						lock (_commonDlls)
						{
							commonDllsKeys = _commonDlls.Keys.ToArray();
						}
						foreach (var dir in commonDllsKeys)
						{
							foreach (var absolutePath in DllsInFolder.Dlls(dir))
							{
								//if (!absSuggested.Contains(absolutePath, StringComparer.InvariantCultureIgnoreCase))
								{
									var goodRef = BuildPath(absolutePath);
									if (goodRef != null)
									{
										//absSuggested.Add(absolutePath);
										yield return new ReferenceFix(goodRef, absolutePath, false);
									}
								}
							}
						}
					}
				}
			}

			string BuildPath(string absolutePath)
			{
				return PathHelper.BuildRelativeFromTo(PathHelper.GetDirectoryName(_fileName), absolutePath, _ctx.SolutionDir, true);
			}

			protected override void ApplyFixCore(Fix fix)
			{
				if (fix != null)
				{
					var referenceFix = (ReferenceFix)fix;
					_reference.ChangeTo(null, referenceFix.HintPath);
					if (!string.IsNullOrEmpty(_reference.HintPath))
					{
						AddMap(Path.GetFileNameWithoutExtension(_reference.HintPath), referenceFix.AbsolutePath);
					}
					// Revalidate();
				}
			}

		}

		public class Reference
		{
			public override string ToString()
			{
				if (IsGac)
				{
					return Include;
				}
				return HintPath;
			}

			readonly string _projectFile;
			readonly string _solutionDir;
			readonly XmlNode _node;
			readonly XmlDoc _doc;

			public Reference(string projectFile, string solutionDir, XmlNode node, XmlDoc doc)
			{
				_projectFile = projectFile;
				_solutionDir = solutionDir;
				_node = node;
				_doc = doc;
			}

			public void ChangeTo(string include, string hint)
			{
				if (include != null)
				{
					_node.Attributes["Include"].Value = include;
				}
				if (hint != null)
				{
					var node = _node.SelectSingleNode("x:HintPath", _doc.Nm);
					if (node == null)
					{
						node = _doc.XmlDocument.CreateElement("HintPath", _doc.NameSpaceMsbuild);
						_node.AppendChild(node);
					}
					node.InnerText = hint;
				}
				_doc.Save();
			}

			public string AbsoluteHint
			{
				get { return CalcAbsoluteHint(_hintPath, _projectFile); }
			}

			public static string CalcAbsoluteHint(string hintPath, string project)
			{
				if (string.IsNullOrEmpty(hintPath))
				{
					return null;
				}
				var cur = PathHelper.GetDirectoryName(project).TrimEnd('\\');
				var path = hintPath;

				while (path.StartsWith("..\\"))
				{
					var i = cur.LastIndexOf('\\');
					if (i < 0)
					{
						// bad hint
						return null;
					}
					path = path.Substring(3);
					cur = cur.Substring(0, i).TrimEnd('\\');
				}
				return Path.Combine(cur, path);
			}

			public string RelativeFromSolution
			{
				get
				{
					var abs = AbsoluteHint;
					if (abs.StartsWith(_solutionDir, StringComparison.InvariantCultureIgnoreCase))
					{
						return abs.Substring(_solutionDir.Length);
					}
					throw new Exception("No way");
				}
			}

			public string Include;
			string _hintPath;

			public string HintPath
			{
				get { return _hintPath; }
				set { _hintPath = value; }
			}

			public bool IsGac
			{
				get { return string.IsNullOrEmpty(HintPath); }
			}

			public bool IsGoodReference
			{
				get
				{
					if (!Path.IsPathRooted(AbsoluteHint))
					{
						return false;
					}
					if (!PathHelper.FileExists(AbsoluteHint))
					{
						return false;
					}
					if (!AbsoluteHint.StartsWith(_solutionDir, StringComparison.InvariantCultureIgnoreCase))
					{
						return false;
					}
					return true;
				}
			}
		}

		bool IsPossibleGAC(Reference reference, bool isTestProject)
		{
			if (isTestProject && reference.Include.StartsWith("Microsoft.VisualStudio.", StringComparison.Ordinal))
			{
				return true;
			}
			if (reference.Include.StartsWith("System.", StringComparison.Ordinal))
			{
				return true;
			}

			// ONLY .NET FRAMEWORK COMPONENTS
			switch (reference.Include)
			{
				case "mscorlib":
				case "WindowsFormsIntegration":
				case "WindowsBase":
				case "PresentationFramework":
				case "PresentationCore":
				case "Microsoft.CSharp":
				case "UIAutomationProvider":
				case "Accessibility":
				case "System":
					return true;
			}

			return false;
		}

		//		public static IEnumerable<XmlNode> GetReferencesFrom(string fileName)
		//		{
		//			var doc = new XmlDocument();
		//			doc.Load(fileName);
		//			return GetReferences(doc);
		//		}
		//
		//		public static IEnumerable<XmlNode> GetReferences(string body)
		//		{
		//			var doc = new XmlDocument();
		//			doc.LoadXml(body);
		//			return GetReferences(doc);
		//		}

		public static IEnumerable<Reference> GetReferences(XmlDoc doc, ValidationContext ctx)
		{
			return
				from node in doc.Select("x:Project/x:ItemGroup/x:Reference")
				let hintNode = node.SelectSingleNode("x:HintPath", doc.Nm)
				select new Reference(doc.FileName, ctx.SolutionDir, node, doc)
				{
					Include = node.Attributes["Include"].Value,
					HintPath = hintNode == null ? null : hintNode.InnerText,
				};
		}

	}
}